package nessie;

/*
 * this class emulates the PPU operations of the NES. the emulation is scanline
 * level based, and the PPU ticks(cycles) are nothing but an urban legend at this point.
 * sections:
 * -registers:
 *  every PPU register is VERY function specific
 * -drawing:
 *  or rather calculating what to draw where, when, why, how many and on who's
 *  authoraton!
 * -mirroring:
 *  the funky mirroring allows the NES to do what made it so great. to scroll!
 *  but while the actual scrolling code is in the draw functions, they couldn't
 *  have done it without the mirrors.
 */
public class PPU {

    /**
     * G E N E R A L
     */
    protected static boolean VERTICAL_MIRROR;
    private static final byte[] VRAM = new byte[0xFFFF];
    private static final byte[] SPR_RAM = new byte[256];//sprite ram
    private static final int[] SECONDARY_OAM = new int[64];//4 bytes per OAM object, but 64bytes long for double height sprites
    private static int loopy_T, loopy_V;//15 bits
    private static byte loopy_X, loopy_Y;//3 bits, fine scroll
    private static int[] palette;
    private static int scanline = 0;
    private static long debugCounter = 0;
    private static boolean overflowFlag = false, spriteZeroHitFlag = false, VBLFlag = false;
    private static byte OAM_POINTER = 0;
    private static MainUI mainUI;

    private PPU() {
    }

    public static void init(MainUI mainUI) {
        palette = new int[3];
        NMI_ME();
        PPU.mainUI = mainUI;
        //clear nametables and above
        for (int a = 0x2000; a < VRAM.length; a++) {
            VRAM[a] = 0;
        }
    }

    public static void loadVRAM(byte[] VROM) {
        System.arraycopy(VROM, 0, VRAM, 0, VROM.length - 1);
    }

    public static void writeVRAMByte(byte data, int address) {
        VRAM[resolveMirror(address)] = data;
    }

    public static byte readVRAMByte(int address) {
        return VRAM[resolveMirror(address)];
    }

    /**
     * clear the toggle that determines if writes to 0x2005 or 0x2006 go into
     * the first or latter 8 bits(they share the same toggle).
     */
    public static void clearFIRST_WRITE_TOGGLE() {
        //this is reversed in the actual nes
        FIRST_WRITE = true;
    }

    /**Non Maskable Interrupt**/
    protected static void NMI_ME() {
        Flags.B = false;
        Stack.pushWideSP(Registers.PC);
        Stack.pushSP(Registers.getP());
        Registers.PC = (short) 0xFFF9;//0xFFFA - 1 because the Memory.ADDRESS_ABSOLUTE() function does a pre-increment
        Registers.PC = Memory.ADDRESS_ABSOLUTE();
    }

    public static void draw(int currentScanline) {
        debugCounter++;
        {
            if ((scanline = currentScanline) == 0) {
                if (isRenderingEnabled()) {
                    loopy_V = loopy_T;//dot 304
                }
                mainUI.setBackgroundColor(Palette.getColorRGBByCode(readVRAMByte(0x3F00)));
                clearFlags();
            }

            if (scanline < 240) {//TODO:sprite0 hit
                if (isRenderingEnabled()) {
                    loopy_V = loopy_T & 0b00001100_00011111;//dot 256
                }
                drawScanlineToRaster();
                return;
            }
            if (scanline == 241) {
                VBL();
                if (NMI_VBL) {
                    NMI_ME();
                }
                mainUI.drawRasterToScreen();
                return;
            }
        }
    }

    private static void drawScanlineToRaster() {
        {//draw background
            if (isSpriteZero) {//if sprite zero not found yet
                isSpriteZero = false;//temporary disable sprite zero check
                drawBackgroundToRaster();
                isSpriteZero = true;//re-enable it
            } else {
                drawBackgroundToRaster();
            }
        }

        {//draw sprites
            drawSprites();
        }

        {
            evaluateSprites();
        }
    }

    private static boolean isRenderingEnabled() {
        return showBackground || showSprites;
    }

    /**@return next Object Attribute Memory (OAM) array*/
    private static int[] getOAM() {
        int[] OAM = new int[4];
        OAM[0] = SPR_RAM[(OAM_POINTER++) & 0xFF] & 0xFF;
        OAM[1] = SPR_RAM[(OAM_POINTER++) & 0xFF] & 0xFF;
        OAM[2] = (SPR_RAM[(OAM_POINTER++) & 0xFF]) & 0xE3;//bit 2 is always 0
        OAM[3] = SPR_RAM[(OAM_POINTER++) & 0xFF] & 0xFF;
        return OAM;
    }

    protected static synchronized void VBL() {
        VBLFlag = true;
    }

    public static void clearVBL() {
        VBLFlag = false;
    }

    protected static boolean isVBL() {
        return VBLFlag;
    }

    private static void clearFlags() {
        overflowFlag = spriteZeroHitFlag = false;
        isSpriteZero = true;
        spriteZeroTileIndex = -1;
    }

    public static byte getFlags() {
        return (byte) ((VBLFlag ? 0b1000_0000 : 0)
                | (spriteZeroHitFlag ? 0b0100_0000 : 0)
                | (overflowFlag ? 0b0010_0000 : 0));
    }
    /**
     *
     * R E G I S T E R S
     * 
     */
    // PPUCTRL
    private static short NAMETABLE;//base name table address                        0b0000_0011
    private static byte INCREMENT_MODE;//vram increment horizontal/vertical    0b0000_0100//TODO:rename
    private static short SPRITETABLE;//sprite pattern table address                 0b0000_1000
    private static short BACKGROUNDTABLE;//background pattern table address         0b0001_0000
    private static byte SPRITESIZE;//sprite size                                    0b0010_0000
    private static boolean NMI_VBL;//generate nmi on vbl                              0b1000_0000
    //-------------------------------------------------------------------------------------------------------------------------------
    // PPUMASK 0 1 2 3 4 5 6 7 grayscale=bit0,...blues=bit7.
    private static boolean grayscale, showBackgroundLeft, showSpritesLeft, showBackground, showSprites, reds, greens, blues;
    //-------------------------------------------------------------------------------------------------------------------------------
    // PPUSCROLL
    private static int $2005;
    private static boolean FIRST_WRITE = true;//this toggle switch is used by both $2005 and $2006
    //-------------------------------------------------------------------------------------------------------------------------------
    // OAMADDR
    private static int $2003;//8 bits
    //-------------------------------------------------------------------------------------------------------------------------------
    private static int X_TILE_OFFSET, Y_TILE_OFFSET;
    private static byte readBuffer = 0;

    private static byte getPPUCTRL() {
        return Memory.readByte(0x2000);
    }

    public static void setPPUCTRL() {
        final byte PPUCTRL = getPPUCTRL();
        final byte nametable = (byte) (PPUCTRL & 0b0000_0011);
        setNAMETABLE(nametable);
        setHORIZONTAL((PPUCTRL & 0b0000_0100) / 0b0000_0100);
        setBACKGROUNDTABLE((PPUCTRL & 0b0001_0000) / 0b0001_0000);
        setSPRITESIZE((PPUCTRL & 0b0010_0000) / 0b0010_0000);
        setNMI((PPUCTRL & 0b1000_0000) / 0b1000_0000);
//        //set bit7
        loopy_T = (loopy_T & 0b01110011_11111111) + ((nametable & 0b11) << 10);//first two bits go into bits 10,11
    }

    public static void setPPUMASK() {
        byte PPUMASK = Memory.readByte(0x2001);
        grayscale = (PPUMASK & 0b0000_0001) == 0b0000_0001;
        showBackgroundLeft = (PPUMASK & 0b0000_0010) == 0b0000_0010;
        showSpritesLeft = (PPUMASK & 0b0000_0100) == 0b0000_0100;
        showBackground = (PPUMASK & 0b0000_1000) == 0b0000_1000;
        showSprites = (PPUMASK & 0b0001_0000) == 0b0001_0000;
        reds = (PPUMASK & 0b0010_0000) == 0b0010_0000;
        greens = (PPUMASK & 0b0100_0000) == 0b0100_0000;
        blues = (PPUMASK & 0b1000_0000) == 0b1000_0000;
    }

    private static void setNAMETABLE(int chasis) {
        switch (chasis) {
            case 1:
                NAMETABLE = 0x2400;
                break;
            case 2:
                NAMETABLE = 0x2800;
                break;
            case 3:
                NAMETABLE = 0x2C00;
                break;
            default:
                NAMETABLE = 0x2000;
                break;
        }
    }

    private static void setHORIZONTAL(int number) {
        if (number == 1) {
            INCREMENT_MODE = 32;//vertical
        } else {
            INCREMENT_MODE = 1;//horizontal
        }
    }

    private static void setSPRITETABLE(int number) {
        if (number == 1) {
            SPRITETABLE = 0x1000;
        } else {
            SPRITETABLE = 0x0000;
        }
    }

    private static void setBACKGROUNDTABLE(int number) {
        if (number == 1) {
            BACKGROUNDTABLE = 0x1000;
        } else {
            BACKGROUNDTABLE = 0x0000;
        }
    }

    private static void setSPRITESIZE(int number) {
        if (number == 1) {
            SPRITESIZE = 16;
        } else {
            SPRITESIZE = 8;
            setSPRITETABLE((getPPUCTRL() & 0b0000_1000) / 0b0000_1000);
        }
    }

    private static void setNMI(int number) {
        if (number == 1) {
            NMI_VBL = true;
        } else {
            NMI_VBL = false;
        }
    }

    public static void set2003() {
        int addressByte = Memory.readByte(0x2003) & 0xFF;
        $2003 = addressByte;
    }

    public static void set2004(byte data) {
        SPR_RAM[$2003++ & 0xFF] = data;
    }

    public static byte get2004() {
        return SPR_RAM[$2003 & 0xFF];
    }

    /**PPUSCROLL*/
    public static int set2005() {
        int addressByte = Memory.readByte(0x2005) & 0xFF;
        if (FIRST_WRITE) {//horizontal
            loopy_T &= 0xFFE0;
            loopy_T |= ((addressByte >>> 3) & 0xFF);//bits 3-7 go into 0-4
            loopy_X = (byte) (addressByte & 0b0111);
            X_TILE_OFFSET = ((addressByte & 0b11111000) >>> 3) * 8;
        } else {//vertical
            loopy_T &= 0xFC1F;
            loopy_T |= ((addressByte & 0b11111000) << 2);//bits 3-7 go into 5-9
            loopy_T &= 0x8FFF;
            loopy_T |= ((addressByte & 0b111) << 12);//bits 0-2 go into 12-14
            loopy_Y = (byte) (addressByte & 0b0111);
            Y_TILE_OFFSET = (((addressByte & 0b11111000) >>> 3) * 8);
        }
        FIRST_WRITE = !FIRST_WRITE;
        return $2005;
    }

    /**PPUADDR*/
    public static int set2006() {
        int addressByte = Memory.readByte(0x2006) & 0xFF;
        if (FIRST_WRITE) {
            loopy_T &= 0xFF;
            loopy_T |= ((addressByte & 0b00111111) << 8);
        } else {
            loopy_T &= 0xFF00;
            loopy_T |= addressByte;
            loopy_V = loopy_T;
        }
        FIRST_WRITE = !FIRST_WRITE;
        return loopy_T;
    }

    /**PPUDATA*/
    public static void set2007(byte addressByte) {
        writeVRAMByte(addressByte, (loopy_V += INCREMENT_MODE) - INCREMENT_MODE);//post-increment for values larger than 1
    }

    /**
     * reads the byte from the 0x2006 VRAM address into the readBuffer and
     * returns the previous readBuffer value
     */
    public static byte get2007() {
        if (loopy_V < 0x3F00) {
            //this way the readBuffer is kept in the pipeline until the next time we access this memory range
            byte oldBuffer = readBuffer;
            readBuffer = readVRAMByte(loopy_V);
            loopy_V += INCREMENT_MODE;
            return oldBuffer;
        } else {
//            http://nesdev.parodius.com/bbs/viewtopic.php?t=1721
            readBuffer = readVRAMByte(loopy_V - 0x1000);
            return readVRAMByte((loopy_V += INCREMENT_MODE) - INCREMENT_MODE);
        }
    }

    public static void set4014() {
        final int pageSize = 0x100;//256
        final int pageNumber = Memory.readByte(0x4014) & 0xFF;
        final int pageOffset = pageNumber * pageSize;
        byte addressByte = 0;
        for (int a = 0; a < SPR_RAM.length; a++) {
            addressByte = Memory.readByte(pageOffset + a);
            SPR_RAM[($2003 + a) & 0xFF] = addressByte;
        }
    }
    /**
     *
     * S P R I T E S
     * 
     */
    private static int spritesPerScanlineCounter = 0;//max 8
    private static boolean isSpriteZero = false;
    private static int spriteZeroTileIndex = -1;
    private static boolean sprite8x16Bottom = false;

    /**draw the current 8 sprites to the buffer*/
    private static void drawSprites() {
        if (showSprites) {
            for (int index = 0; index < spritesPerScanlineCounter; index++) {
                int y = SECONDARY_OAM[(index * 4) + 0] + 1;//y coordinate is scanline -1
                int tileIndex = SECONDARY_OAM[(index * 4) + 1];
                int attrib = SECONDARY_OAM[(index * 4) + 2];
                int x = SECONDARY_OAM[(index * 4) + 3];

                if (y > scanline) {
                    continue;
                }

                //pattern table entry 16 bytes
                byte[] tile = new byte[16];

                if (SPRITESIZE == 8) {
                    System.arraycopy(VRAM, resolveMirror(SPRITETABLE + (tileIndex * 16)), tile, 0, 16);
                } else {//else if (SPRITESIZE == 16) {
                    final int bank = (tileIndex & 1) * 0x1000;
                    int bla = sprite8x16Bottom ? 1 : 0;
                    System.arraycopy(VRAM, resolveMirror(bank + (((tileIndex & 0b11111110) + bla) * 16)), tile, 0, 16);
                }

                boolean verticalFlip = (attrib & 0b1000_0000) == 0b1000_0000;
                boolean horizontalFlip = (attrib & 0b0100_0000) == 0b0100_0000;
                boolean backgroundPriority = (attrib & 0b0010_0000) == 0b0010_0000;
                int upperColorbits = (attrib & 0b0011);//4-7

                if (horizontalFlip) {
                    for (int b = 0; b < tile.length; b++) {
                        byte temp = (byte) (Integer.reverse(tile[b]) >> 24);//8 bits are reversed as 32bit int
                        tile[b] = temp;
                    }
                }

                if (verticalFlip) {
                    byte[] temp = new byte[16];
                    temp[0] = tile[7];
                    temp[1] = tile[6];
                    temp[2] = tile[5];
                    temp[3] = tile[4];
                    temp[4] = tile[3];
                    temp[5] = tile[2];
                    temp[6] = tile[1];
                    temp[7] = tile[0];

                    temp[8] = tile[15];
                    temp[9] = tile[14];
                    temp[10] = tile[13];
                    temp[11] = tile[12];
                    temp[12] = tile[11];
                    temp[13] = tile[10];
                    temp[14] = tile[9];
                    temp[15] = tile[8];
                    tile = temp;
                }
                getSpritePalette(upperColorbits);

                if (isSpriteZero == false) {//not first line of sprite
                    if (!spriteZeroHitFlag//no sprite0hit found yet
                            && ((spriteZeroTileIndex == x + (y * 256))//current tile
                            || (sprite8x16Bottom && spriteZeroTileIndex == x + (y * 256) - 2048))) {
                        isSpriteZero = true;
                    }
                }
                drawSpriteTileLine(x, y, tile, backgroundPriority);
            }
        }
    }

    /**draw the current (scan)line of the sprite*/
    private static void drawSpriteTileLine(int x, int y, byte[] tileLine, boolean isSpriteBehind) {//TODO:remove y
        final int index = scanline - y;//set the current line of the sprite to be drawn

        y = scanline;//set y coordinate

        for (int xx = 0; xx < 8; xx++) {
            if (!showSpritesLeft && x + xx < 8) {
                continue;
            }
            byte currentLeftTile = tileLine[index];
            byte currentRightTile = tileLine[index + 8];
            int mask = 128 >>> xx;
            isSpriteBehind &= mainUI.isBackgroundPixelOpaque(x + xx, y);
            if ((mask & currentLeftTile) == mask) {
                if ((mask & currentRightTile) == mask) {//both
                    sprite0Hit(x + xx, y, palette[2]);
                    if (isSpriteBehind) {//invisibile if background not invisible
                        continue;
                    }
                    mainUI.drawToBuffer(x + xx, y, palette[2]);
                } else {//left
                    sprite0Hit(x + xx, y, palette[0]);
                    if (isSpriteBehind) {
                        continue;
                    }
                    mainUI.drawToBuffer(x + xx, y, palette[0]);
                }
            } else if ((mask & currentRightTile) == mask) {//right
                sprite0Hit(x + xx, y, palette[1]);
                if (isSpriteBehind) {
                    continue;
                }
                mainUI.drawToBuffer(x + xx, y, palette[1]);
            }
        }
        isSpriteZero = false;
    }

    /**detect a hit in sprite 0 of the OAM*/
    private static void sprite0Hit(int x, int y, int color) {
        if (isSpriteZero && showSprites && showBackground && color != 0 && x < 255) {
            isSpriteZero = !(spriteZeroHitFlag = mainUI.isBackgroundPixelOpaque(x, y));
        }
    }

    private static void getSpritePalette(int paletteNumber) {
        int paletteIndex = 0x3F11;
        switch (paletteNumber) {
            case 0:
                break;
            case 1:
                paletteIndex = 0x3F15;
                break;
            case 2:
                paletteIndex = 0x3F19;
                break;
            case 3:
                paletteIndex = 0x3F1D;
                break;
            default://error
                throw new UnsupportedOperationException("bad sprite palette number.");
        }
        //each palette is 4 bytes, but the first byte(0, transparency) is ignored
        for (int a = 0; a < 3; a++) {
            palette[a] = Palette.getColorRGBByCode(readVRAMByte(paletteIndex + a));
        }
    }

    /**evaluate sprites for <b>NEXT</b> scanline*/
    private static void evaluateSprites() {
        spritesPerScanlineCounter = OAM_POINTER = 0;
        for (int a = 0; a < 64; a++) {
            //byte0 y position -1
            //byte1 tile index
            //byte2 attrib
            //byte3 x position, no wraparound
            int[] OAM = getOAM();
            int x = OAM[3];
            int y = OAM[0];

            int range = scanline - y;

            //difference 0-7 is within range(0-15 for 8x16 tiles)
            if (range > -1 && range < SPRITESIZE) {
                if (spritesPerScanlineCounter < 8) {//always evaluate sprites, but only render when rendering is enabled
                    System.arraycopy(OAM, 0, SECONDARY_OAM, spritesPerScanlineCounter * 4, 4);
                    {
                        sprite8x16Bottom = false;
                        if (SPRITESIZE == 16 && scanline >= y + 8) {//get part 2 of the sprite
                            OAM[0] = y += 8;//y coordinate is 8 pixels below the first part
                            sprite8x16Bottom = true;
                            System.arraycopy(OAM, 0, SECONDARY_OAM, spritesPerScanlineCounter * 4, 4);
                        }
                    }
                }
                spritesPerScanlineCounter++;//overflow happens at 9

                if (spritesPerScanlineCounter > 7) {
                    spritesPerScanlineCounter = 8;//for the drawScanlineToRaster loop
                    overflowFlag = isRenderingEnabled();
                    break;
                }
            }

            if (a == 0 && !spriteZeroHitFlag) {//sprite 0
                spriteZeroTileIndex = x + ((y + 1) * 256);
            }
        }
    }

    /**
     *
     * B A C K G R O U N D
     *
     */
    private static void drawBackgroundToRaster() {
        if (showBackground) {
            //32*30
            byte[] backgroundTile = new byte[16];
            int tileOffset = (((scanline + Y_TILE_OFFSET + (loopy_Y > 0 ? 8 : 0)) / 8)) * 32;//1 tile = 8x8 pixels, 1 scanline = 32 tiles

            ////////TODO:fine vertical scrolling

            if (Y_TILE_OFFSET > 0 || loopy_Y > 0) {
                for (int a = 0; a < tileOffset; a++, loopy_V++) {//vertical scrolling
                    if ((loopy_V & 928) == 928) {//vertical scrolling wraps around at 29(//0b00000011_10100000)
                        //if 30
                        //then 00
                        loopy_V &= ~0b00000011_11100000;
                        loopy_V ^= 0x800;//bit 11
                    }
                    if ((loopy_V & 0b00000011_11100000) == 0b00000011_11100000) {//if manually set to value above 29(928)
                        loopy_V &= ~0b00000011_11100000;//wraparound but dont switch tables
                    }
                }
            } else {
                loopy_V += tileOffset;
            }

            tileOffset %= 960;
            int EOL = tileOffset + 32 + 1;

            //X,Y of left corner of scanline
            int nametableAddress = (0x2000 | (loopy_V & 0b00001100_00000000));//0xc00
            int x = ((tileOffset & 0x1F) << 3) - X_TILE_OFFSET - loopy_X;//32 tiles per row, and 8 bits per tile

            for (int tileIndex = tileOffset; tileIndex < EOL; ++tileIndex, x += 8) {
                if (x + loopy_X > -1) {//in range
                    {//horizontal scrolling
                        if (((loopy_V & 31) == 0) && tileIndex != tileOffset) {//bits 0-4
                            loopy_V ^= 0x400;//bit 10
                            loopy_V -= 32;//go up a line
                            EOL = EOL - tileIndex + tileOffset;
                            tileIndex = tileOffset;//beginning of page
                            nametableAddress = (0x2000 | (loopy_V & 0b00001100_00000000));//^= 0x400
                        }
                    }
                    int nametableEntry = (readVRAMByte(nametableAddress + tileIndex) & 0xFF) * 16;
                    getBackgroundPalette(tileIndex);
                    System.arraycopy(VRAM, resolveMirror(BACKGROUNDTABLE + nametableEntry), backgroundTile, 0, 16);

                    drawBackgroundTileLine(x, backgroundTile);
                    loopy_V++;
                } else {//out of range
                    EOL++;//get extra tile for scrolling
                }
            }
        }
    }

    /**Draws a line of background tiles on the <b>CURRENT</b> scanline.*/
    private static void drawBackgroundTileLine(int x, byte[] tileLine) {
        final int index = scanline & 7;//%8

        for (int xx = 0; xx < 8; xx++) {
            if (!showBackgroundLeft && x + xx < 8) {//hide background in leftmost 8 pixels
                continue;
            }
            byte currentLeftTile = tileLine[index];
            byte currentRightTile = tileLine[index + 8];
            int mask = 128 >>> xx;
            if ((mask & currentLeftTile) == mask) {
                if ((mask & currentRightTile) == mask) {//both
                    mainUI.drawToTransparencyBuffer(x + xx, scanline, palette[2]);
                } else {//left
                    mainUI.drawToTransparencyBuffer(x + xx, scanline, palette[0]);
                }
            } else if ((mask & currentRightTile) == mask) {//right
                mainUI.drawToTransparencyBuffer(x + xx, scanline, palette[1]);
            }
        }
    }

    private static void getBackgroundPalette(int nametableIndex) {
        /**
         * nametableIndex 1 byte = 8 bit tile 1 column = 32 bits 1 attribute
         * byte = 4 areas 1 area = 4 bytes 32 bytes per row 30 bytes per column
         */
        int bitX = (nametableIndex & 0x1F) & 3;//32 tiles per row, 4 tiles per column, 2 quadrants(0=first,1=second)
        int bitY = (nametableIndex >> 5) & 3;//32 tiles per row, 4 tiles per column, 2 quadrants(0=first,1=second)

        //if 2bits are in the middle of a byte, get the complete number of bytes.
        int actualNametable = (loopy_V & 0xc00) | 0x2000;
        int actualAttributeTable = actualNametable + 0x3c0;

        int xx = loopy_V & 0x1F;
        int yy = ((loopy_V >> 5) & 0x1F);
        int entry = VRAM[resolveMirror(actualAttributeTable + (xx >> 2) + 8 * (yy >> 2))];

        int paletteNumber = (bitX + bitY * 4);
        int paletteIndex = 0x3F01;
        /**
         * 0 1|2 3 4 5|6 7 ---|--- 8 9|A B C D|E F
         */
        switch (paletteNumber) {
            case 0:
            case 1:
            case 4:
            case 5:
                entry = (entry & 0b0000_0011) >> 0;
                break;
            case 2:
            case 3:
            case 6:
            case 7:
                entry = (entry & 0b0000_1100) >> 2;
                break;
            case 8:
            case 9:
            case 12:
            case 13:
                entry = (entry & 0b0011_0000) >> 4;
                break;
            case 10:
            case 11:
            case 14:
            case 15:
                entry = (entry & 0b1100_0000) >> 6;
                break;
            default://error
                throw new UnsupportedOperationException("bad background palette number.");
        }
        switch (entry) {
            case 0:
                break;
            case 1:
                paletteIndex = 0x3F05;
                break;
            case 2:
                paletteIndex = 0x3F09;
                break;
            case 3:
                paletteIndex = 0x3F0D;
                break;
            default://error
                throw new UnsupportedOperationException("bad background palette number.");
        }
        //each palette is 4 bytes, but the first byte is ignored
        for (int a = 0; a < 3; a++) {
            palette[a] = Palette.getColorRGBByCode(VRAM[paletteIndex + a]);
        }
    }

    /**
     *
     * M I R R O R I N G
     *
     */
    private static int resolveMirror(int address) {
        address &= 0x3FFF;
        //TODO:check mirrors
        if (address >= 0x3F00) {//sprite palette mirror(0x3F10-0x3F20)
            address &= 0x3F1F;
            if (address == 0x3F10 || address == 0x3F14 || address == 0x3F18 || address == 0x3F1C) {
                return address - 0x10;
            }
            return address;
        }
        if (address >= 0x3000) {//mirror of name & attribute tables(0x2000-0x2FFF)
            address -= 0x1000;
        }
        if (VERTICAL_MIRROR) {
            //vertical mirroring
            if (address >= 0x2C00) {//name table 3 mirror of 1
                return address - 0x800;
            }
            if (address >= 0x2800) {//name table 2 mirror of 0
                return address - 0x800;
            }
        } else {
            //horizontal mirroring
            if (address >= 0x2C00) {//name table 3 mirror of 2
                return address - 0x400;
            }
            if (address >= 0x2800) {//name table 2
                return address;
            }
            if (address >= 0x2400) {//name table 1 mirror of 0
                return address - 0x400;
            }
        }
        //default
        return address;
    }
}
